﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.Net;
using System.IO;
using System.Globalization;
using System.Text.RegularExpressions;

namespace Porter
{
    public class Service
    {
        private object fileLock = new object();
        public string ServiceCachePath { get; private set; }
        public string LastRequestUrl { get; private set; }

        public Service(Uri url)
        {
            ServiceCachePath = String.Format("{0}\\{1}", Global.Settings.Get("ApplicationDataPath") as string, "ServiceCache");
            Url = url;
            ResponseCache = new Dictionary<Function, CacheValue>();
        }

        [Serializable]
        public struct CacheValue
        {
            [XmlElement]
            public Response Response;
            [XmlElement]
            public DateTime LastUpdate;
        }

        public Dictionary<Function, CacheValue> ResponseCache;

        #region MessageClasses

        /* Objects for "func 1" */

        [Serializable]
        public class Station
        {
            [XmlAttribute]
            public int stId;
            [XmlAttribute]
            public string stName;
        }

        [Serializable]
        public class StationLine
        {
            [XmlAttribute]
            public string coName;
            [XmlAttribute]
            public int stId;
            [XmlAttribute]
            public string stName;
            [XmlAttribute]
            public double stLat;
            [XmlAttribute]
            public double stLng;
            [XmlAttribute]
            public int lnId;
            [XmlAttribute]
            public int ltId;
            [XmlAttribute]
            public string lnNumber;
            [XmlAttribute]
            public int slRoute;
            [XmlElement]
            public Station EndStations;
        }

        [Serializable]
        public class _StationLines
        {
            [XmlElement]
            public List<StationLine> StationLine;

            public _StationLines()
            {
                StationLine = new List<StationLine>();
            }
        }

        /* Objects for "func 2" */

        [Serializable]
        public class Line
        {
            [XmlAttribute]
            public int coId;
            [XmlAttribute]
            public string coName;
            [XmlAttribute]
            public int lnId;
            [XmlAttribute]
            public int ltId;
            [XmlAttribute]
            public string lnNumber;
            [XmlElement]
            public int endId1;
            [XmlElement]
            public string endName1;
            [XmlElement]
            public int endId2;
            [XmlElement]
            public string endName2;
        }

        [Serializable]
        public class _Lines
        {
            [XmlElement]
            public List<Line> Line;

            public _Lines()
            {
                Line = new List<Line>();
            }
        }

        /* Objects for "func 3" */

        [Serializable]
        public class Timetable
        {
            [XmlAttribute]
            public int teId;
            [XmlAttribute]
            public string teName;
            [XmlAttribute]
            public string teDateFrom;
            [XmlAttribute]
            public string teDateTo;
            [XmlAttribute]
            public int ttId;
            [XmlAttribute]
            public string ttDay;
            [XmlAttribute]
            public int ttHour;
            [XmlAttribute]
            public int ttFixMin;
            [XmlAttribute]
            public int ttMinMin;
            [XmlAttribute]
            public int ttMaxMin;
        }

        [Serializable]
        public class StationDetail
        {
            [XmlAttribute]
            public int lnId;
            [XmlAttribute]
            public int stId;
            [XmlAttribute]
            public string stName;
            [XmlAttribute]
            public double stLat;
            [XmlAttribute]
            public double stLng;
            [XmlAttribute]
            public int slNumber;
            [XmlAttribute]
            public int slTime;
            [XmlAttribute]
            public int slRoute;
            [XmlElement]
            public List<Timetable> Timetable;

            public StationDetail()
            {
                Timetable = new List<Timetable>();
            }
        }

        [Serializable]
        public class LineInfo
        {
            [XmlElement]
            public string coName;
            [XmlElement]
            public int lnId;
            [XmlElement]
            public string lnType;
            [XmlElement]
            public string lnNumber;
            [XmlElement("Station")]
            public List<StationDetail> Stations;

            public LineInfo()
            {
                Stations = new List<StationDetail>();
            }
            // itt amúgy lesz egy kis gebasz a start-end vs. termhatárral...
            public List<DateTime> GetDepartures(int count)
            {
                int c = count;
                List<DateTime> departures = new List<DateTime>();
                List<int> terms = new List<int>();
                Response r = Global.m_service.Request(Function.Terms, new Parameters()
                {
                    lnId = new List<int>() { this.lnId }
                });
                if (r.code == (int)Function.Terms)
                {
                    foreach (Term t in r.Terms.Term)
                    {
                        foreach (TermDate td in t.TermDate)
                        {
                            if (DateTime.Parse(td.teDateFrom).CompareTo(DateTime.Now) <= 0
                                && DateTime.Parse(td.teDateTo).CompareTo(DateTime.Now) >= 0)
                            {
                                terms.Add(t.teId);
                            }
                        }
                    }
                    // now check if the entries are valid for today (base time)
                    foreach (Timetable tt in this.Stations[0].Timetable)
                    {
                        if (terms.Contains(tt.teId) && tt.ttDay.Contains(((int)DateTime.Today.DayOfWeek + 1 % 7).ToString())
                            && tt.ttHour > DateTime.Now.Hour && c >= 0)
                        {
                            departures.Add(new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
                                                        tt.ttHour, tt.ttFixMin, 0));
                            c--;
                        }
                    }
                }
                else System.Windows.Forms.MessageBox.Show(String.Format("Could not query terms for line {0} (id={1}).", this.lnNumber, this.lnId));
                return departures;
            }
        }

        /* Objects for func 4 */

        [Serializable]
        public class Stations
        {
            [XmlElement("Station")]
            public List<StationDetail> StationList;

            public Stations()
            {
                StationList = new List<StationDetail>();
            }
        }

        /* Objects for func 5 */

        [Serializable]
        public class TermDate
        {
            [XmlAttribute]
            public string teDateFrom;
            [XmlAttribute]
            public string teDateTo;
        }

        [Serializable]
        public class Term
        {
            [XmlElement]
            public int teId;
            [XmlElement]
            public string teName;
            [XmlElement]
            public List<TermDate> TermDate;

            public Term()
            {
                TermDate = new List<TermDate>();
            }
        }

        [Serializable]
        public class Terms
        {
            [XmlElement]
            public List<Term> Term;

            public Terms()
            {
                Term = new List<Term>();
            }
        }

        [Serializable]
        [XmlRoot]
        public class Response
        {
            /* Header - general response attributes */
            [XmlAttribute]
            public int code;
            [XmlAttribute]
            public string message;
            [XmlAttribute]
            public string details;

            /* Payload */
            [XmlElement("StationLines")]
            public _StationLines StationLines;
            [XmlElement("Lines")]
            public _Lines Lines;
            [XmlElement("LineInfo")]
            public LineInfo LineInfo;
            [XmlElement]
            public Terms Terms;

            public Response()
            {
                StationLines = new _StationLines();
                Lines = new _Lines();
                LineInfo = new LineInfo();
                Terms = new Terms();
            }
        }

        #endregion

        #region Environment

        private Uri Url { get; set; }

        private NumberFormatInfo numberFormat = CultureInfo.InvariantCulture.NumberFormat;

        #endregion

        #region RemoteFunctions

        public enum Function
        {
            StationLines = 1, Lines, LineInfo, Stations, Terms, LineTypes,
            StationSubs, StationTimetables
        };

        public Dictionary<Function, string> Functions = new Dictionary<Function, string>()
        {
            {Function.StationLines, "StationLines"},
            {Function.Lines, "Lines"},
            {Function.LineInfo, "LineInfo"},
            {Function.Stations, "Stations"},
            {Function.Terms, "Terms"},
            {Function.LineTypes, "LineTypes"}
        };

        public struct Parameters
        {
            public double? stLat;
            public double? stLng;
            public float? dist;
            public List<int> lnId;
            public DateTime? date;
        }

        private string getCacheName(Function _func, Parameters _param)
        {
            StringBuilder filename = new StringBuilder(Functions[_func]);
            filename.Append("#");
            if (_param.stLat != null && _param.stLat.HasValue) filename.Append(_param.stLat);
            filename.Append("#");
            if (_param.stLng != null && _param.stLng.HasValue) filename.Append(_param.stLng);
            filename.Append("#");
            if (_param.dist != null && _param.dist.HasValue) filename.Append(_param.dist);
            filename.Append("#");
            if (_param.lnId != null && _param.lnId.Count > 0) filename.Append(_param.lnId[0]);
            filename.Append("#");
            if (_param.date != null && _param.date.HasValue) filename.Append(_param.date);
            return Regex.Replace(filename.ToString(), @"[?:\/*""<>|]", "_");
        }

        private bool toCache(Function _func, Parameters _param)
        {
            return true;
        }

        public Response Request(Function _func, Parameters _param)
        {
            System.Windows.Forms.Cursor.Show();
            System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.WaitCursor;
            Response r = new Response { code = 0 };

            string cacheFilePath = String.Format("{0}\\{1}.xml", ServiceCachePath, getCacheName(_func, _param));

            if (File.Exists(cacheFilePath))
            {
                try
                {
                    using (FileStream fs = new FileStream(cacheFilePath, FileMode.Open))
                    {
                        ResponseCache[_func] = (CacheValue)
                                               (new XmlSerializer(typeof(CacheValue)).Deserialize(fs));
                    }
                }
                catch (Exception x)
                {
                    System.Windows.Forms.MessageBox.Show(x.Message);
                }
            }
            else
            {

                // TODO: check if the internet connectivity is disabled -> only cache

                StringBuilder requestUrl = new StringBuilder(Url.ToString(), 255);
                requestUrl.Append("?client=1&func=" + Functions[_func]);
                // Append parameters
                switch (_func)
                {
                    case Function.StationLines:
                        if (_param.stLat.HasValue && _param.stLng.HasValue)
                            requestUrl.AppendFormat(numberFormat, "&stLat={0:0.####}&stLng={1:0.####}", _param.stLat,
                                                    _param.stLng);
                        if (_param.dist.HasValue)
                            requestUrl.AppendFormat(numberFormat, "&dist={0}", _param.dist);
                        break;
                    case Function.Lines:
                        break;
                    case Function.Terms:
                    case Function.LineInfo:
                        if (_param.lnId != null && _param.lnId.Count == 1)
                            requestUrl.AppendFormat(numberFormat, "&lnId={0}", _param.lnId[0]);
                        r.LineInfo.lnId = _param.lnId[0];
                        break;
                    case Function.Stations:
                        requestUrl.Append("?func=GetStations");
                        requestUrl.AppendFormat(numberFormat, "&date={0:yyyyMMdd}&stLat={1:0.####}&stLng={2:0.####}",
                                                _param.date, _param.stLat, _param.stLng);
                        foreach (int lnId in _param.lnId)
                        {
                            requestUrl.AppendFormat(numberFormat, "&lnId[]={0}", lnId);
                        }
                        break;
                    default:
                        return new Response() { code = 0 };
                }

                LastRequestUrl = requestUrl.ToString();
                try
                {
                    HttpWebRequest req = (HttpWebRequest)WebRequest.Create(requestUrl.ToString());
                    req.Timeout = 5000;
                    r = (Response)new XmlSerializer(typeof(Response)).Deserialize(
                        ((HttpWebResponse)req.GetResponse()).GetResponseStream()
                    );
                }
                catch (Exception x)
                {
                    if (x is TimeoutException) System.Windows.Forms.MessageBox.Show("Connection timeout!");
                    else System.Windows.Forms.MessageBox.Show("Invalid server response!");
                    
                    ResponseCache[_func] = new CacheValue()
                    {
                        Response = new Response() { code = 0, details = x.Message }
                    };
                }
                ResponseCache[_func] = new CacheValue()
                {
                    Response = r,
                    LastUpdate = DateTime.Now
                };

                // save cache entry to file if needed (TODO, now always)
                if (ResponseCache[_func].Response.code > 0 && toCache(_func, _param))
                {
                    try
                    {
                        using (FileStream fs = new FileStream(cacheFilePath, FileMode.Create))
                        {
                            new XmlSerializer(typeof(CacheValue)).Serialize(fs, ResponseCache[_func]);
                        }
                    }
                    catch (Exception)
                    {
                        System.Diagnostics.Debug.WriteLine("Failed to cache response");
                    }
                    // We are happy
                }
            }
            System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.Default;
            System.Windows.Forms.Cursor.Hide();
            return ResponseCache[_func].Response;
        }

        public void ClearCache()
        {
            if (Directory.Exists(ServiceCachePath))
            {
                Directory.Delete(ServiceCachePath);
            }
        }

        #endregion

    }
}
